package com.aratana.ui.fields;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ActionMapUIResource;

import com.aratana.ui.InputChangeListener;
import com.aratana.ui.ValueChangeListener;
import com.aratana.ui.ViewField;

@SuppressWarnings("serial")
public class CheckField extends JCheckBox implements ViewField<Boolean> {

	private final TristateDecorator model;

	private final List<ValueChangeListener<Boolean>> listeners = new ArrayList<ValueChangeListener<Boolean>>();
	private final List<InputChangeListener> input = new ArrayList<InputChangeListener>();

	public CheckField(String text, Icon icon, Boolean initial) {
		super(text, icon);
		// Add a listener for when the mouse is pressed
		super.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				grabFocus();
				model.nextState();
			}
		});
		// Reset the keyboard action map
		ActionMap map = new ActionMapUIResource();
		map.put("pressed", new AbstractAction() {
			public void actionPerformed(ActionEvent e) {
				grabFocus();
				model.nextState();
			}
		});
		map.put("released", null);
		SwingUtilities.replaceUIActionMap(this, map);
		// set the model to the adapted model
		model = new TristateDecorator(getModel());
		setModel(model);
		setValue(initial);
	}

	public CheckField(String text, Boolean initial) {
		this(text, null, initial);
	}

	public CheckField(String text) {
		this(text, null);
	}

	public CheckField() {
		this(null);
	}

	/** No one may add mouse listeners, not even Swing! */
	public void addMouseListener(MouseListener l) {
	}

	@Override
	public void setSelected(boolean b) {
		setValue(b);
	}

	/**
	 * Exactly which Design Pattern is this? Is it an Adapter, a Proxy or a
	 * Decorator? In this case, my vote lies with the Decorator, because we are
	 * extending functionality and "decorating" the original model with a more
	 * powerful model.
	 */
	private class TristateDecorator implements ButtonModel {
		private final ButtonModel other;

		private TristateDecorator(ButtonModel other) {
			this.other = other;
		}

		private void setState(Boolean state) {
			if (state == null) {
				other.setArmed(true);
				setPressed(true);
				setSelected(true);
			} else {
				if (state) {
					other.setArmed(false);
					setPressed(false);
					setSelected(true);
				} else {
					other.setArmed(false);
					setPressed(false);
					setSelected(false);
				}
			}
		}

		/**
		 * The current state is embedded in the selection / armed state of the
		 * model.
		 * 
		 * We return the SELECTED state when the checkbox is selected but not
		 * armed, DONT_CARE state when the checkbox is selected and armed (grey)
		 * and NOT_SELECTED when the checkbox is deselected.
		 */
		private Boolean getState() {
			if (isSelected() && !isArmed()) {
				// normal black tick
				return Boolean.TRUE;
			} else if (isSelected() && isArmed()) {
				// don't care grey tick
				return null;
			} else {
				// normal deselected
				return Boolean.FALSE;
			}
		}

		/** We rotate between NOT_SELECTED, SELECTED and DONT_CARE. */
		private void nextState() {
			Boolean current = getState();
			if (current == null) {
				setState(Boolean.FALSE);
			} else {
				if (current) {
					setState(null);
				} else {
					setState(Boolean.TRUE);
				}
			}
		}

		/** Filter: No one may change the armed status except us. */
		public void setArmed(boolean b) {
		}

		/**
		 * We disable focusing on the component when it is not enabled.
		 */
		public void setEnabled(boolean b) {
			setFocusable(b);
			other.setEnabled(b);
		}

		/**
		 * All these methods simply delegate to the "other" model that is being
		 * decorated.
		 */
		public boolean isArmed() {
			return other.isArmed();
		}

		public boolean isSelected() {
			return other.isSelected();
		}

		public boolean isEnabled() {
			return other.isEnabled();
		}

		public boolean isPressed() {
			return other.isPressed();
		}

		public boolean isRollover() {
			return other.isRollover();
		}

		public void setSelected(boolean b) {
			other.setSelected(b);
		}

		public void setPressed(boolean b) {
			other.setPressed(b);
		}

		public void setRollover(boolean b) {
			other.setRollover(b);
		}

		public void setMnemonic(int key) {
			other.setMnemonic(key);
		}

		public int getMnemonic() {
			return other.getMnemonic();
		}

		public void setActionCommand(String s) {
			other.setActionCommand(s);
		}

		public String getActionCommand() {
			return other.getActionCommand();
		}

		public void setGroup(ButtonGroup group) {
			other.setGroup(group);
		}

		public void addActionListener(ActionListener l) {
			other.addActionListener(l);
		}

		public void removeActionListener(ActionListener l) {
			other.removeActionListener(l);
		}

		public void addItemListener(ItemListener l) {
			other.addItemListener(l);
		}

		public void removeItemListener(ItemListener l) {
			other.removeItemListener(l);
		}

		public void addChangeListener(ChangeListener l) {
			other.addChangeListener(l);
		}

		public void removeChangeListener(ChangeListener l) {
			other.removeChangeListener(l);
		}

		public Object[] getSelectedObjects() {
			return other.getSelectedObjects();
		}
	}

	@Override
	public void addValueChangeListener(ValueChangeListener<Boolean> listener) {
		listeners.add(listener);
	}

	@Override
	public void addInputChangeListener(InputChangeListener listener) {
		input.add(listener);
	}

	public boolean removeInputChangeListener(final InputChangeListener listener) {
		return input.remove(listener);
	}

	public void removeAllInputChangeListener() {
		input.clear();
	}

	protected void fireInputChanged() {
		for (InputChangeListener l : input) {
			l.inputChange(this);
		}
	}

	@Override
	public int getMax() {
		return 0;
	}

	@Override
	public Boolean getValue() {
		return model.getState();
	}

	@Override
	public boolean isObrigatory() {
		return false;
	}

	@Override
	public Boolean setValue(Boolean value) {
		Boolean old = model.getState();
		if (old != value) {
			if (value != null) {
				if (!value.equals(old)) {
					return verifyValueAndSet(value, old);
				}
			} else {
				if (!old.equals(value)) {
					return verifyValueAndSet(value, old);
				}
			}
		}
		return old;
	}

	private Boolean verifyValueAndSet(Boolean value, Boolean old) {
		boolean isValid = true;
		for (ValueChangeListener<Boolean> l : listeners) {
			isValid = l.validateValue(value);
			if (!isValid) {
				break;
			}
		}
		if (isValid) {
			fireInputChanged();
			model.setState(value);
			for (ValueChangeListener<Boolean> l : listeners) {
				l.valueChanged(value, old);
			}
			return value;
		}
		return old;
	}

	@Override
	public Class<Boolean> getFieldClass() {
		return Boolean.class;
	}

	public static void main(String[] args) {
		try {
			UIManager.setLookAndFeel(UIManager.getInstalledLookAndFeels()[0].getClassName());
		} catch (Exception e) {

		}
		JFrame frame = new JFrame("teste Tri state");
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		frame.add(new CheckField("testes"));
		frame.pack();
		frame.setVisible(true);
	}

	@Override
	public int getMin() {
		return 0;
	}

	@Override
	public Boolean getStringValue(String str) throws Exception {
		return Boolean.parseBoolean(str);
	}

	@Override
	public String getValueString(Boolean value) {
		return value == null ? null : value.toString();
	}
}
